package com.geekydoo;

import generated.ObjectFactory;
import generated.TaskListContainer;
import generated.TaskType;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import java.util.logging.Logger;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;

import com.geekydoo.commands.Command;
import com.geekydoo.commands.UndoCommand;
import com.rits.cloning.Cloner;

/**
 * The purpose of this class is to set up and provide access to the XML storage
 * system used by the program. Additionally, this class executes the command
 * objects returned from the CommandParser.
 * 
 * @author Wai Yau, Patrick Dutch
 */
public final class ActionHandler {

	private static Logger logger = Logger.getLogger("geekydoo");

	// JAXB-related variables
	private JAXBContext jaxbContext;
	private Marshaller marshaller;
	private TaskListContainer taskListContainer;

	// Parses user input and returns corresponding command objects
	private CommandParser commandParser;

	// The singleton reference to an instance of this class
	private static ActionHandler actionHandler;

	// List of all tasks managed by the program
	private List<TaskType> tasks;

	// List of all tasks as it was before the last command which modified the
	// list
	private static List<TaskType> preModificationTasks;

	// Strings used by JAXB to initialize storage
	private static final String NAME_JAXB_PACKAGE = "generated";
	private static final String NAME_XML_FILE = "geekydo.xml";

	// Error messages printed by ActionHandler
	private static final String ERROR_ADD_SAVE_TOOL = "Error with creating the tools to add/save tasks to XML file.";
	private static final String ERROR_FILE_NOT_FOUND = "XML file not found.";
	private static final String ERROR_SAVE = "Error with saving tasks to XML file.";

	// Invalid user input messages
	private static final String INVALID_COMMAND = "Command not recognized. Try the help command!";

	// Prompts printed out by this class
	private static final String PROMPT_CONFIRM_DELETE =
			"Fatal error: Resetting the program may allow it to recover from the error."
					+ "\nWould you like to reset the program, which will delete your current task list? (y/n) ";

	// Valid user input expressions
	private static final String INPUT_YES = "((?i)yes)|((?i)y)";
	private static final String INPUT_NO = "((?i)no)|((?i)n)";

	/**
	 * The constructor sets up the storage to be used in the application.
	 * 
	 * @throws GeekyDooException If an error occurs while trying to initialize
	 *             the storage
	 */
	private ActionHandler() throws GeekyDooException {
		commandParser = CommandParser.getCommandParser();

		try {
			setUpStorage();
		}
		// Try to recover from a JAXBException (likely to be caused by a
		// corruption in the XML file the program uses) by deleting the XML file
		// after receiving the user's permission. The recovery was successful if
		// we don't receive another JAXBException while trying to set up the
		// storage for the second time.
		catch (JAXBException e) {
			try {
				logger.warning("There was a JAXBException. We'll see if the user wants to reset the task list XML file.");
				String answer = InputOutputHandler.getInputFromConsole(PROMPT_CONFIRM_DELETE, false);
				while (!InputValidation.isValidYesNoResponse(answer)) {
					answer = InputOutputHandler.getInputFromConsole(PROMPT_CONFIRM_DELETE, false);
				}
				if (answer.matches(INPUT_YES)) {
					File geekyDoXml = new File(NAME_XML_FILE);
					boolean fileDeleted = geekyDoXml.delete();
					logger.warning("The user chose to delete the XML file. File delete status: " + fileDeleted);
				}
				else if (answer.matches(INPUT_NO)) {
					logger.warning("The user chose not to delete the XML file");
				}
				logger.warning("Reattempting to initialize storage");
				setUpStorage();
			}
			catch (JAXBException e1) {
				GeekyDooException gde = new GeekyDooException(ERROR_ADD_SAVE_TOOL, e1);
				throw gde;
			}
		}

		// Set the currently null preModificationTasks list used by the undo
		// command to the current list, so undo won't cause problems when there
		// was no last change to undo
		Cloner cloner = new Cloner();
		preModificationTasks = cloner.deepClone(tasks);
	}

	/**
	 * This method sets up the JAXB storage system that will be used by the
	 * program.
	 * 
	 * @throws JAXBException If an error occurs while parsing the XML storage
	 *             file
	 * @throws GeekyDooException If an IO exception occurs while trying to read
	 *             from the XML storage file
	 */
	private void setUpStorage() throws JAXBException, GeekyDooException {
		jaxbContext = JAXBContext.newInstance(NAME_JAXB_PACKAGE);
		logger.info("Got a JAXBContext");
		assert (jaxbContext != null);

		Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();
		logger.info("Got an Unmarshaller");
		assert (unmarshaller != null);

		File geekyDoXml = new File(NAME_XML_FILE);
		if (geekyDoXml.isFile()) {
			try {
				InputStream inputStream = null;
				try {
					inputStream = new FileInputStream(geekyDoXml);
					logger.info(geekyDoXml.getAbsolutePath() + " is a file so we'll use it.");
					taskListContainer = (TaskListContainer) unmarshaller.unmarshal(inputStream);
				}
				finally {
					if (inputStream != null) {
						inputStream.close();
					}
				}
			}
			catch (IOException e) {
				GeekyDooException gde = new GeekyDooException(ERROR_ADD_SAVE_TOOL, e);
				throw gde;
			}
			logger.info("Unmarshalled taskListContainer from XML file");
		}
		else {
			logger.info("No storage XML file exists right now, JAXB will create one.");
			ObjectFactory of = new ObjectFactory();
			taskListContainer = of.createTaskListContainer();
			logger.info("Got a new empty taskListContainer");
		}
		assert (taskListContainer != null);

		tasks = taskListContainer.getTask();
		logger.info("Got the list of tasks from the taskListContainer");
		assert (tasks != null);

		marshaller = jaxbContext.createMarshaller();
		logger.info("Got a Marshaller");
		assert (marshaller != null);

		marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, new Boolean(true));
	}

	/**
	 * This method provides the ActionHandler to be used.
	 * 
	 * @return the ActionHandler to be used
	 * @throws GeekyDooException If an error occurs while trying to construct
	 *             the ActionHandler
	 */
	public static ActionHandler getActionHandler() throws GeekyDooException {
		if (actionHandler == null) {
			actionHandler = new ActionHandler();
		}
		assert (actionHandler != null);
		logger.info("Returning the ActionHandler");
		return actionHandler;
	}

	/**
	 * This method save the changes made (from user actions) to storage.
	 * 
	 * @throws GeekyDooException If an error occurs while trying to save the
	 *             task list to XML
	 */
	public void save() throws GeekyDooException {
		logger.info("Preparing to save the taskList back to XML");
		FileOutputStream out = null;
		try {
			out = new FileOutputStream(NAME_XML_FILE);
			marshaller.marshal(taskListContainer, out);
		}
		catch (FileNotFoundException e) {
			GeekyDooException gde = new GeekyDooException(ERROR_FILE_NOT_FOUND, e);
			throw gde;
		}
		catch (JAXBException e) {
			GeekyDooException gde = new GeekyDooException(ERROR_SAVE, e);
			throw gde;
		}
		finally {
			if (out != null) {
				try {
					out.close();
				}
				catch (IOException e) {
					GeekyDooException gde = new GeekyDooException(ERROR_SAVE, e);
					throw gde;
				}
			}
		}
		logger.info("Successfully saved the taskList back to XML");
	}

	/**
	 * This method executes the appropriate command.
	 * 
	 * @param commandString User input string to determine which command to
	 *            execute
	 * @throws GeekyDooException If an error occurs during or after command
	 *             execution
	 */
	public void execute(String commandString) throws GeekyDooException {
		Command commandObject = commandParser.parseCommand(commandString);
		if (commandObject != null) {
			commandObject.execute(tasks);
			if (commandObject instanceof UndoCommand) {
				tasks.clear();
				tasks.addAll(preModificationTasks);
				logger.info("The task list has been reverted to the premodification version");
			}
			logger.info("Returned from command execution of \"" + commandString + "\" successfully");
			save();

			if (commandObject.isListModified()) {
				logger.info("List was modified so we'll keep a reference to the original");
				preModificationTasks = commandObject.getPreExecutionList();
			}
		}
		// commandObject == null indicates the command parser couldn't parse
		// the user input to a command
		else {
			InputOutputHandler.printToConsole(INVALID_COMMAND, true, false);
		}
	}

}
